﻿using zijian666.WorkScheduler.Abstractions;


namespace zijian666.WorkScheduler.Defaults;

/// <summary>
/// 令牌桶默认实现
/// </summary>
/// <param name="capacity">桶容量</param>
/// <param name="expires">过期策略</param>
public class TokenBucket(int capacity, IExpirationPolicy expires) : ITokenBucket
{
    private readonly Token[] _tokens = new Token[capacity];
    private readonly IExpirationPolicy _expires = expires;

    public IToken? TryGet()
    {
        var result = new Token(this);
        for (int i = 0; i < _tokens.Length; i++)
        {
            var token = _tokens[i];
            if (_expires.IsExpired(token))
            {
                if (Interlocked.CompareExchange(ref _tokens[i]!, result, token) == token)
                {
                    return result;
                }
            }
        }
        return null;
    }

    public bool Return(object token)
        => Return(token as Token);

    private bool Return(Token? token)
    {
        if (token is null || _expires.IsExpired(token))
        {
            return false;
        }
        for (int i = 0; i < _tokens.Length; i++)
        {
            if (Interlocked.CompareExchange(ref _tokens[i]!, null, token) == token)
            {
                token.TryDispose();
                return true;
            }
        }
        return false;
    }


    public int Free => Tokens().Count(t => _expires.IsExpired(t));

    public int Used => Tokens().Count(t => !_expires.IsExpired(t));

    public int Capacity => _tokens.Length;

    private IEnumerable<Token> Tokens()
    {
        for (int i = 0; i < _tokens.Length; i++)
        {
            yield return _tokens[i];
        }
    }

    private class Token(TokenBucket bucket) : DisposableBase, IToken
    {
        public DateTimeOffset CreatedTime { get; } = DateTimeOffset.UtcNow;

        private static long _sequence = long.MinValue;

        public ulong Sequence { get; } = (ulong)(Interlocked.Increment(ref _sequence) - long.MinValue);

        protected override void Dispose(bool disposing)
            => bucket.Return(this);

        public DateTimeOffset LastActiveTime { get; private set; } = DateTimeOffset.UtcNow;

        public void Activation()
        {
            if (bucket._expires.IsExpired(this))
            {
                this.TryDispose();
                throw new InvalidOperationException("Token is expired.");
            }

            LastActiveTime = DateTimeOffset.UtcNow;
        }

        public IExpirationPolicy Expiration => bucket._expires;

        public override string ToString()
        {
            return $"SEQ: {Sequence}, LAT: {LastActiveTime:u}, EXP: {bucket._expires.IsExpired(this)}";
        }
    }
}
